/*
 * To change this license header, choose License Headers in Project Properties.
 * To change this template file, choose Tools | Templates
 * and open the template in the editor.
 */
package cz.muni.fi.pb138.xml.wrda.core;

import java.io.Serializable;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import org.jdom2.Document;
import org.jdom2.Element;
import org.jdom2.Namespace;
import org.jdom2.filter.Filters;
import org.jdom2.output.Format;
import org.jdom2.output.XMLOutputter;
import org.jdom2.xpath.XPathExpression;
import org.jdom2.xpath.XPathFactory;

/**
 *
 * @author Ondřej Chrastina 430596@mail.muni.cz
 */
public class XSD implements Serializable {

    private Long timeStamp;
    private String fileName;
    private List<SimpleType> simpleTypes;
    private List<ComplexType> complexTypes;
    //key = name of elements and value is list of atributes
    private Map<String, List<String>> elements;
    private Document content;

    public Long getTimeStamp() {
        return timeStamp;
    }

    public void setTimeStamp(Long timeStamp) {
        this.timeStamp = timeStamp;
    }

    public List<SimpleType> getSimpleTypes() {
        return simpleTypes;
    }

    public void setSimpleTypes(List<SimpleType> simpleTypes) {
        this.simpleTypes = simpleTypes;
    }

    public List<ComplexType> getComplexTypes() {
        return complexTypes;
    }

    public void setComplexTypes(List<ComplexType> complexTypes) {
        this.complexTypes = complexTypes;
    }

    public Map<String, List<String>> getElements() {
        return elements;
    }

    public void setElements(Map<String, List<String>> elements) {
        this.elements = elements;
    }

    public Document getContent() {
        return content;
    }

    public void setContent(Document content) {
        this.content = content;
    }

    public String getFileName() {
        return fileName;
    }

    public void setFileName(String fileName) {
        this.fileName = fileName;
    }

    public String getXSDContent() {
        XMLOutputter outputter = new XMLOutputter(Format.getPrettyFormat());
        String output = outputter.outputString(content.getRootElement());
        return output;
    }

    @Override
    public int hashCode() {
        int hash = 3;
        hash = 89 * hash + Objects.hashCode(this.timeStamp);
        return hash;
    }

    @Override
    public boolean equals(Object obj) {
        if (obj == null) {
            return false;
        }
        if (getClass() != obj.getClass()) {
            return false;
        }
        final XSD other = (XSD) obj;
        if (!(Objects.equals(this.timeStamp, other.timeStamp) && Objects.equals(this.fileName, other.fileName))) {
            return false;
        }
        return true;
    }

    public XSD() {
        timeStamp = System.currentTimeMillis();
        return;
    }

    public void parseElementsFromDocument() {
        //Getting a root document
        XPathExpression<Element> xsdDocumentXpathExpression = XPathFactory.instance().compile("//xsd:schema", Filters.element(), new HashMap<String, Object>(0), Namespace.getNamespace("xsd", "http://www.w3.org/2001/XMLSchema"));
        //XPathExpression<Element> xsdDocumentXpathExpression = XPathFactory.instance().compile("//schema", Filters.element());
        Element xsdRootElement = xsdDocumentXpathExpression.evaluateFirst(this.content);

        if (xsdRootElement == null) {
            throw new InternalError("XSD content does not contains xsd:schema root element.");
        }

        XPathExpression<Element> elementsListXpathExpression = XPathFactory.instance().compile("//xsd:element[not(@ref)]", Filters.element(), new HashMap<String, Object>(0), Namespace.getNamespace("xsd", "http://www.w3.org/2001/XMLSchema"));
        XPathExpression<Element> relativeLocalComplexTypeXpathExpression = XPathFactory.instance().compile("./xsd:complexType", Filters.element(), new HashMap<String, Object>(0), Namespace.getNamespace("xsd", "http://www.w3.org/2001/XMLSchema"));
//        XPathExpression<Element> elementsListXpathExpression = XPathFactory.instance().compile("//element", Filters.element());
//        XPathExpression<Element> relativeLocalComplexTypeXpathExpression = XPathFactory.instance().compile("./complexType", Filters.element());

        List<Element> elementsList = elementsListXpathExpression.evaluate(xsdRootElement);
        elements = new HashMap<>(elementsList.size());

        List<String> elementAtribuiteNames;
        Element localComplexTypeElement;
        for (Element elementItem : elementsList) {
            localComplexTypeElement = relativeLocalComplexTypeXpathExpression.evaluateFirst(elementItem);
            elementAtribuiteNames = getAtributeNamesFromElement(localComplexTypeElement);
            elements.put(elementItem.getAttributeValue("name"), elementAtribuiteNames);
        }
    }

    private List<String> getAtributeNamesFromElement(Element elementComplexType) {
        XPathExpression<Element> elementAttributesXpathExpression = XPathFactory.instance().compile("./xsd:attribute", Filters.element(), new HashMap<String, Object>(0), Namespace.getNamespace("xsd", "http://www.w3.org/2001/XMLSchema"));
        XPathExpression<Element> elementAttributeGroupXpathExpression = XPathFactory.instance().compile("./xsd:attributeGroup", Filters.element(), new HashMap<String, Object>(0), Namespace.getNamespace("xsd", "http://www.w3.org/2001/XMLSchema"));
        XPathExpression<Element> elementAnyAttributeXpathExpression = XPathFactory.instance().compile("./xsd:anyAttribute", Filters.element(), new HashMap<String, Object>(0), Namespace.getNamespace("xsd", "http://www.w3.org/2001/XMLSchema"));

        List<String> ret = new ArrayList<>();

        //Getting local elements name from element's complex type
        List<Element> localElementAtributes = elementAttributesXpathExpression.evaluate(elementComplexType);
        String name;
        for (Element localElementAttribute : localElementAtributes) {
            if ((name = localElementAttribute.getAttributeValue("ref")) != null) { // the type is only reference, converting to definition
                localElementAttribute = convertReferencedAttributeToDefinition(localElementAttribute, name);
            }

            if (localElementAttribute.getAttributeValue("name") == null || localElementAttribute.getAttributeValue("name").equals("")) {
                ret.add(Type.NO_NAME_TYPE);
            } else {
                ret.add(localElementAttribute.getAttributeValue("name"));
            }
        }

        List<Element> localAttributeGroups = elementAttributeGroupXpathExpression.evaluate(elementComplexType);
        for (Element localAttributeGroup : localAttributeGroups) {
            String localGroupName;
            if ((localGroupName = localAttributeGroup.getAttributeValue("ref")) != null) {
                localAttributeGroup = convertReferencedAttributeGroupToDefinition(localAttributeGroup, localGroupName);
            }
            ret.addAll(getAtributeNamesFromElement(localAttributeGroup));
        }

        //max 1 anyAttribute is allowed
        if (elementAnyAttributeXpathExpression.evaluateFirst(elementComplexType) != null) {
            ret.add("anyAttribute");
        }

        return ret;
    }

    private Element convertReferencedAttributeToDefinition(Element localElementAttribute, String name) {
        XPathExpression<Element> namedAttribute = XPathFactory.instance().compile(MessageFormat.format("//xsd:attribute[@name=\"{0}\"]", name), Filters.element(), new HashMap<String, Object>(0), Namespace.getNamespace("xsd", "http://www.w3.org/2001/XMLSchema"));
        return namedAttribute.evaluateFirst(localElementAttribute);

    }

    private Element convertReferencedAttributeGroupToDefinition(Element localAttributeGroup, String name) {
        XPathExpression<Element> namedAttributeGroup = XPathFactory.instance().compile(MessageFormat.format("//xsd:attributeGroup[@name=\"{0}\"]", name), Filters.element(), new HashMap<String, Object>(0), Namespace.getNamespace("xsd", "http://www.w3.org/2001/XMLSchema"));
        return namedAttributeGroup.evaluateFirst(localAttributeGroup);
    }

    public void parseSimpleTypesFromDocument() {

        XPathExpression<Element> simpleTypesXpathExpression = XPathFactory.instance().compile("//xsd:simpleType", Filters.element(), new HashMap<String, Object>(0), Namespace.getNamespace("xsd", "http://www.w3.org/2001/XMLSchema"));
//        XPathExpression<Element> simpleTypesXpathExpression = XPathFactory.instance().compile("//simpleType", Filters.element());

        //Getting a root document
        XPathExpression<Element> xsdDocumentXpathExpression = XPathFactory.instance().compile("//xsd:schema", Filters.element(), new HashMap<String, Object>(0), Namespace.getNamespace("xsd", "http://www.w3.org/2001/XMLSchema"));
//        XPathExpression<Element> xsdDocumentXpathExpression = XPathFactory.instance().compile("//schema", Filters.element());
        Element xsdRootElement = xsdDocumentXpathExpression.evaluateFirst(this.content);

        if (xsdRootElement == null) {
            throw new InternalError("XSD content does not contains xsd:schema root element.");
        }

        //Finding all simple types elements
        List<Element> simpleTypesElements = simpleTypesXpathExpression.evaluate(xsdRootElement);
        SimpleType item;
        simpleTypes = new ArrayList<>();
        for (Element simpleTypeElement : simpleTypesElements) {
            simpleTypes.add(new SimpleType(simpleTypeElement));
        }

    }

    public void parseComplexTypesFromDocument() {
        XPathExpression<Element> complexTypesXpathExpression = XPathFactory.instance().compile("//xsd:complexType[not(@ref)]", Filters.element(), new HashMap<String, Object>(0), Namespace.getNamespace("xsd", "http://www.w3.org/2001/XMLSchema"));
//        XPathExpression<Element> complexTypesXpathExpression = XPathFactory.instance().compile("//simpleType", Filters.element());

        XPathExpression<Element> xsdDocumentXpathExpression = XPathFactory.instance().compile("//xsd:schema", Filters.element(), new HashMap<String, Object>(0), Namespace.getNamespace("xsd", "http://www.w3.org/2001/XMLSchema"));
        Element xsdRootElement = xsdDocumentXpathExpression.evaluateFirst(this.content);

        if (xsdRootElement == null) {
            throw new InternalError("XSD content does not contains xsd:schema root element.");
        }

        //Finding all complex types elements
        List<Element> complexTypesElements = complexTypesXpathExpression.evaluate(xsdRootElement);
        ComplexType item;
        complexTypes = new ArrayList<>();
        for (Element complexTypeElement : complexTypesElements) {
            complexTypes.add(new ComplexType(complexTypeElement));

        }

    }

    public class Type {

        private static final String NO_NAME_TYPE = "NOT SPECIFIED";
        private String name;
        private String declaration;

        public Type() {
        }

        public String getName() {
            if (name == "") {
                return NO_NAME_TYPE;
            } else {
                return name;
            }
        }

        public void setName(String name) {
            this.name = name;
        }

        public String getDeclaration() {
            return declaration;
        }

        public void setDeclaration(String declaration) {
            this.declaration = declaration;
        }

    }

    //Class to store complex type of XSD
//<simpleType
//id=ID
//name=NCName
//any attributes
//>
    public class SimpleType extends Type {

        private long id;
        private String contentType;

        public SimpleType() {
        }

        private SimpleType(Element simpleTypeElement) {

            XPathExpression<Element> restrictionXpathExpression = XPathFactory.instance().compile("./xsd:restriction", Filters.element(), new HashMap<String, Object>(0), Namespace.getNamespace("xsd", "http://www.w3.org/2001/XMLSchema"));
            XPathExpression<Element> listXpathExpression = XPathFactory.instance().compile("./xsd:list", Filters.element(), new HashMap<String, Object>(0), Namespace.getNamespace("xsd", "http://www.w3.org/2001/XMLSchema"));
            XPathExpression<Element> unionXpathExpression = XPathFactory.instance().compile("./xsd:union", Filters.element(), new HashMap<String, Object>(0), Namespace.getNamespace("xsd", "http://www.w3.org/2001/XMLSchema"));
//            
//            XPathExpression<Element> restrictionXpathExpression = XPathFactory.instance().compile("./restriction", Filters.element());
//            XPathExpression<Element> listXpathExpression = XPathFactory.instance().compile("./list", Filters.element());
//            XPathExpression<Element> unionXpathExpression = XPathFactory.instance().compile("./union", Filters.element());

            //Setting declaration
            XMLOutputter writer = new XMLOutputter();
            super.declaration = writer.outputString(simpleTypeElement);

            String id;
            if ((id = simpleTypeElement.getAttributeValue("id")) != null) {
                this.id = Long.parseLong(id);
            }

            String name = simpleTypeElement.getAttributeValue("name");
            if (name == null || name.equals("")) {
                super.setName(super.NO_NAME_TYPE);
            } else {
                super.setName(name);
            }

            Element restriction;
            if ((restriction = restrictionXpathExpression.evaluateFirst(simpleTypeElement)) != null) {
                contentType = restriction.getAttributeValue("base");
            } else if (listXpathExpression.evaluateFirst(simpleTypeElement) != null) {
                contentType = "list";
            } else if (unionXpathExpression.evaluateFirst(simpleTypeElement) != null) {
                contentType = "union";
            } else {
                contentType = super.NO_NAME_TYPE;
            }
        }

        public long getId() {
            return id;
        }

        public void setId(long id) {
            this.id = id;
        }

        public String getContentType() {
            return contentType;
        }

        public void setContentType(String contentType) {
            this.contentType = contentType;
        }

    }

    //Class to store complex type of XSD
//<complexType
//id=ID
//name=NCName
//abstract=true|false
//mixed=true|false
//block=(#all|list of (extension|restriction))
//final=(#all|list of (extension|restriction))
//any attributes
//>
    public class ComplexType extends Type {

        public ComplexType() {
        }

        private ComplexType(Element complexTypeElement) {

            //Setting declaration
            XMLOutputter writer = new XMLOutputter();
            super.declaration = writer.outputString(complexTypeElement);

            String name = complexTypeElement.getAttributeValue("name");
            if (name == null || name.equals("")) {
                super.setName(super.NO_NAME_TYPE);
            } else {
                super.setName(name);
            }

        }

    }

}
